import _ from 'lodash';
// import Power from '#/classes/buffer/power';
// import Collect from '#/classes/net/collect';
import CONFIG from '#/globals/config';

/**
 * 请求设置对象结构：
 * {
 *      url: string,                            // url地址,
 *      data: object,                           // 参数键值对,      
 *      pipe: {                                 // http处理管道（注意：此结构下的所有方法，都可直接在根下使用（如果根下有同名方法，则会覆盖pipe中的方法））
 *          start() {},                         // 开始请求时执行的方法
 *          end() {},                           // 请求完成时执行的方法
 *          success(data, obj) {},              // 成功时执行的方法
 *                                              // 非法访问
 *          nopower(res, obj) {},               // 没有权限
 *          error(res, obj) {},                 // 失败时执行的方法（专指逻辑失败）
 *          fail(res, obj) {},                  // 服务器请求失败（40X，50X，返回格式错误等等）
 *          offline() {}                        // 没有联网时执行的方法
 *      }
 * }
 */
export default class Http {

    /**
     * 和社区get请求
     * 
     * @param {Object} obj 设置对象 
     */
    static get(obj) { 
        obj = this._setConfig(obj);
        if (!obj) return {state: 'fail'};
        return this._checkNet('GET', obj);
    }

    /**
     * 和社区post请求
     * 
     * @param {Object} obj 设置对象 
     */
    static post(obj) {
        obj = this._setConfig(obj);
        if (!obj) return {state: 'fail'};
        return this._checkNet('POST', obj);
    }

    /**
     * 原始get请求
     * 
     * @param {Object} obj 设置对象 
     */
    static getOrigin(obj) {
        obj = this._setConfig(obj, true);       // 第二个参数为false，代表原始访问（不加url前缀、不设auths、不解析返回值）
        if (!obj) return {state: 'fail'};
        return this._checkNet('GET', obj);
    }

    /**
     * 原始post请求
     * 
     * @param {Object} obj 设置对象 
     */
    static postOrigin(obj) {
        obj = this._setConfig(obj, true);       // 第二个参数为false，代表原始访问（不加url前缀、不设auths、不解析返回值）
        if (!obj) return {state: 'fail'};
        return this._checkNet('POST', obj);
    }

    /**
     * 访问网络
     * 
     * @param {String} method 访问方法
     * @param {Object} obj 设置对象 
     */
    static _checkNet(method, obj) {
        return new Promise(async (resolve, reject)=> {
            const result = await this._request(Object.assign(obj, {method: method}));
            resolve(result);
        });
        // return new Promise((resolve, reject)=> {

        //     // 判断是否已联网
        //     Taro.getNetworkType({ complete: async (res)=> {

        //         // 执行成功，判断如果没有网络，则调用offline函数，并返回offline
        //         if(res.errMsg == 'getNetworkType:ok' && res.networkType == 'none') {
        //             if (_.isFunction(obj.pipe.offline)) {
        //                 obj.pipe.offline();
        //             } 
        //             resolve({state: 'offline'});

        //         // 否则发起http访问
        //         } else {
        //             const result = await this._request(Object.assign(obj, {method: method}));
        //             resolve(result);
        //         }
        //     }});
        // });
    }

    /**
     * 真实访问方法
     * 
     * @param {Object} obj 设置对象 
     */
    static _request(obj) {
        return new Promise(async (resolve, reject)=> {

            // 执行开始回调
            this._start(obj);

            // ------------------------------------------------------ 
            // 设置url（主要用于get）
            if (obj.method == 'GET' && _.isObject(obj.data)) {
                obj.url = obj.url + this._paramGet(obj.url, obj.data);
            }
            
            // ------------------------------------------------------ 
            // 设置body（主要用于post）
            // 注：如果不需要添加body字段，则Param.post函数返回的是null
            if (obj.method == 'POST') {
                let result = this._paramPost(obj.data);
                if (result != null) {
                    obj['body'] = result;
                }
            }
            
            // ------------------------------------------------------ 
            // 发起请求，并返回数据
            // 判断是否存在服务器状态码错误，最后将文本类型的返回值交给下一步
            fetch(obj.url, obj).then((res)=> {

                console.log('fetch')

                // 在debug模式下，输出response信息
                console.log('--------- ' + obj.method + ' : ' + obj.url);
                if (_.isObject(obj.data)) {
                    console.log(obj.data);
                }
                console.log(res);
                console.log('--------- END');

                // 取状态码
                const statusCode = parseInt(res.status);

                // 状态码出错，调用管道函数，返回状态出错，并返回给下一步null（下一个then使用）
                if (statusCode != 200) {
                    if (_.isFunction(obj.pipe.fail)) {
                        obj.pipe.fail(res, obj);
                    }
                    resolve({state: 'fail'});
                    return null;
                }

                // 将数据返回给下一步，注意，这里全部返回为text类型
                return res.text(); 
            })

            // ------------------------------------------------------ 
            // 数据处理
            .then(async (text)=> {

                // 执行结束回调
                this._end(obj);

                // 空值判断，返回码不为200时，text为null，则直接返回
                if (text == null) return;
                
                // obj.isOrigin为true，直接返回text
                if (obj.isOrigin) {
                    resolve(text);
                }

                // hesq请求，返回解析后的结果
                else {
                    const result = await this._parse(text, obj);
                    resolve(result);
                }
            })

            // ------------------------------------------------------ 
            // 错误抓取
            .catch((err) => {
                
                // 执行结束回调
                this._end(obj);

                // 一但抓取到错误，立即报出，用于调试（正式环境会收集数据）
                console.error(err);

                // 调用失败管道函数
                if (_.isFunction(obj.pipe.fail)) {
                    obj.pipe.fail(err, obj);
                }

                // 返回状态失败
                resolve({state: 'fail'});
            });
        })
    }

    /**
     * 对返回文本的解析（仅和社区访问服务端可用）
     * 
     * @param {String} text 从服务器返回的原始字符串
     * @param {Object} obj 设置对象
     */
    static _parse(text, obj) {
        return new Promise((resolve, reject)=> {

            // 判断字符串是否能被解析为json对象，如果成功则将text转换为json，否则报fail
            // taro有时会自动将字符串转换为json，所以，这里需要做判断，看text是否为string，如果已经是json，就不要转转了
            if (_.isString(text)) {
                try {
                    text = JSON.parse(text);
                } 
                
                // 转换出错，调用管道函数，记录错误、返回fail
                catch(e) {
                    console.error(e);
                    if (_.isFunction(obj.pipe.fail)) {
                        obj.pipe.fail(text, obj);
                    }
                    resolve({state: 'fail'});
                    return;
                }
            }

            // 转换为json
            const json = _.isString(text) ? JSON.parse(text) : text;

            /**
             * 注意：arrive处理
             */
            // if (_.isObject(json.arrive)) {
            //     Power.login(sign);
            //     Collect.arrive();
            // }

            // json格式出错，调用管道函数，返回fail
            if (!_.has(json, 'code') || !_.has(json, 'msg')) {
                if (_.isFunction(obj.pipe.fail)) {
                    obj.pipe.fail(json, obj);
                }
                resolve({state: 'fail'});
            }

            // 逻辑判断，如果code不等于0，调用管道函数，返回error
            else if (parseInt(json.code) != 0) {
                if (_.isFunction(obj.pipe.error)) {
                    obj.pipe.error(json, obj);
                }
                resolve({state: 'error', data: json});
            }

            // 已经全部正确正确，调用success回调
            else {
                resolve({state: 'success', data: json.data});
            }
        });
    }

    /**
     * 检查obj参数，并使用obj根下的pipe函数，覆盖掉obj.pipe下的pipe函数
     * 
     * @param {Object} obj http请求设置对象
     */
    static _setConfig(obj, isOrigin = false) {

        // 检查http请求对象中的：url, pipe是否全部存在
        if (!_.isString(obj.url) || !_.isObject(obj.pipe)) {
            console.error('http请求对象参数obj，至少包含：url, pipe两个参数。且url必须为String类型，pipe必须为Object类型！', obj);
            return false;
        }

        // 设置isOrigin、url、auths
        obj['isOrigin'] = isOrigin;
        obj['url'] = isOrigin ? obj['url'] : CONFIG.Url.restful + obj['url'];
        obj['headers'] = {
            'content-type': 'application/x-www-form-urlencoded',
            'Authorization': isOrigin ? '' : '' // JSON.stringify(Power.getHttpAuths());
        };

        // obj根下管道函数，覆盖obj.pipe管道函数
        const customePipe = _.omit(obj, ['url', 'data', 'pipe']);
        obj['pipe'] = Object.assign(obj.pipe, customePipe);

        // 返回
        return obj;
    }

    /**
     * 设置Get方法的参数
     * 
     * @param {String} url 此次访问的url
     * @param {Object} data 数据对象
     * @return {String} 需要连接到url之后的querystring
     */
    static _paramGet(url, data) {

        // 如果不是平面对象，则直接返回空串
        if (!_.isPlainObject(data)) {
            return '';
        }

        // 计算params数组
        let params = [];
        for (let key in data) {
            params.push(key + '=' + data[key]);
        }

        // 计算得到连接符号和查询字符串
        const symbol = url.indexOf('?') === -1 ? '?' : '&';
        const query = params.join('&');

        // 返回连接在url之后需要连接的参数字符串
        return symbol + query;
    }

    /**
     * 设置Post方法的参数
     * 
     * @param {Object} data 数据对象
     * @return {FormData} 如果存在data，则返回新的formData数据，否则返回null
     */
    static _paramPost(data) {

        // 如果不是平面对象，则直接返回false（代表不需要在obj添加body）
        if (!_.isPlainObject(data)) {
            return null;
        }

        // 制作formData对象，并返回
        let formData = new FormData();
        for (let key in data) {
            formData.append(key, data[key])
        }

        // 返回formData数据（代表需要在obj添加body）
        return formData;
    }

    /**
     * 如果带有指示器，则显示指示器
     * 注意start和end都必须是方法
     * 
     * @param {Object} obj 设置对象
     */
    static _start(obj) {
        if (_.isFunction(obj.pipe.start)) {
            obj.pipe.start();
        }
    }

    /**
     * 如果带有指示器，则隐藏指示器
     * 注意start和end都必须是方法
     * 
     * @param {Object} obj 设置对象
     */
    static _end(obj) {
        if (_.isFunction(obj.pipe.end)) {
            obj.pipe.end();
        }
    }
}